using System;

namespace Sencha.Serialization.Bson
{
	/// <summary>
	/// Representation of MongoDB object's identifier in BSON. This type is immutable by design
	/// </summary>
	[TypeSerializer(typeof(Serializers.BsonObjectIdSerializer))]
	public struct BsonObjectId : IEquatable<BsonObjectId>, IComparable, IComparable<BsonObjectId>
	{
		public static readonly BsonObjectId Empty = new BsonObjectId(0, 0, 0);

		internal int timeStamp;
		internal int machineAndPid;
		internal int pidAndIncrement;

		public int Timestamp { get { return this.timeStamp; } }
		public int Machine { get { return (this.machineAndPid & 0xFFFFFF); } }
		public int Increment { get { return pidAndIncrement << 8; } }
		public short Pid { get { return (short)(ushort)(((uint)this.machineAndPid >> 16) | ((uint)this.pidAndIncrement & 0xFF)); } }
		public DateTime CreationTime { get { return BsonDateTime.Epoch.AddSeconds((double)this.Timestamp); } }

		public BsonObjectId(byte[] value)
		{
			if (value == null)
				throw new ArgumentNullException("value");
			if (value.Length != 12)
				throw new ArgumentOutOfRangeException("Value should be array of bytes with length 12", "value");

			this.timeStamp = BitConverter.ToInt32(value, 0);
			this.machineAndPid = BitConverter.ToInt32(value, 4);
			this.pidAndIncrement = BitConverter.ToInt32(value, 8);
		}
		public BsonObjectId(int first, int second, int third)
		{
			this.timeStamp = first;
			this.machineAndPid = second;
			this.pidAndIncrement = third;
		}

		public override int GetHashCode()
		{
			return this.timeStamp ^ this.machineAndPid ^ this.pidAndIncrement;
		}
		public override bool Equals(object o)
		{
			if (o is BsonObjectId)
				return Equals((BsonObjectId)o);
			return false;
		}
		public bool Equals(BsonObjectId other)
		{
			return this == other;
		}

		public static BsonObjectId Parse(string value)
		{
			var id = Empty;
			if (value == null)
				throw new ArgumentNullException("value");
			if (value.Length != 24)
				throw new ArgumentOutOfRangeException("value");

			var success = true;
			var data1 = HexStringToInt(value, 0, ref success);
			var data2 = HexStringToInt(value, 8, ref success);
			var data3 = HexStringToInt(value, 16, ref success);

			if (!success)
				throw new FormatException();

			id = new BsonObjectId(data1, data2, data3);
			return id;
		}

		public static bool TryParse(string value, out BsonObjectId id)
		{
			id = Empty;
			if (value == null || value.Length != 24)
				return false;

			var success = true;
			var data1 = HexStringToInt(value, 0, ref success);
			var data2 = HexStringToInt(value, 8, ref success);
			var data3 = HexStringToInt(value, 16, ref success);

			id = new BsonObjectId(data1, data2, data3);
			return success;
		}

		public static bool operator ==(BsonObjectId a, BsonObjectId b)
		{
			return a.timeStamp == b.timeStamp && a.machineAndPid == b.machineAndPid && a.pidAndIncrement == b.pidAndIncrement;
		}
		public static bool operator !=(BsonObjectId a, BsonObjectId b)
		{
			return !(a == b);
		}
		public static explicit operator string(BsonObjectId oid)
		{
			return oid.ToString();
		}
		public static explicit operator BsonObjectId(string oidString)
		{
			BsonObjectId oid = Parse(oidString);
			return oid;
		}

		#region IComparable Members

		public int CompareTo(object obj)
		{
			if (!(obj is BsonObjectId))
				return 1;

			return this.CompareTo((BsonObjectId)obj);
		}

		#endregion

		#region IComparable<BsonObjectId> Members

		public int CompareTo(BsonObjectId other)
		{
			var cmp = (this.Timestamp.CompareTo(other.Timestamp));
			if (cmp != 0)
				return cmp;
			cmp = (this.Machine.CompareTo(other.Machine));
			if (cmp != 0)
				return cmp;
			cmp = (this.Pid.CompareTo(other.Pid));
			if (cmp != 0)
				return cmp;

			return this.Increment.CompareTo(other.Increment);
		}

		#endregion

		private static int HexStringToInt(string str, int start, ref bool success)
		{
			const int ZERO = (ushort)'0';
			const int a = (ushort)'a';
			const int A = (ushort)'A';
			const int len = 8;

			var result = (uint)0;
			for (var i = 0; i < len; i++)
			{
				var c = str[start + i];
				var d = (uint)0;

				if (c >= '0' && c <= '9')
					d = (uint)(c - ZERO);
				else if (c >= 'a' && c <= 'f')
					d = (uint)(10 + (c - a));
				else if (c >= 'A' && c <= 'F')
					d = (uint)(10 + (c - A));
				else
					success = false;

				result = checked(16 * result + d);
			}

			return unchecked((int)result);
		}

		public byte[] ToBytes()
		{
			var buffer = new byte[12];

			buffer[0] = (byte)this.timeStamp;
			buffer[1] = (byte)(this.timeStamp >> 8);
			buffer[2] = (byte)(this.timeStamp >> 0x10);
			buffer[3] = (byte)(this.timeStamp >> 0x18);

			buffer[4] = (byte)this.machineAndPid;
			buffer[5] = (byte)(this.machineAndPid >> 8);
			buffer[6] = (byte)(this.machineAndPid >> 0x10);
			buffer[7] = (byte)(this.machineAndPid >> 0x18);

			buffer[8] = (byte)this.pidAndIncrement;
			buffer[9] = (byte)(this.pidAndIncrement >> 8);
			buffer[10] = (byte)(this.pidAndIncrement >> 0x10);
			buffer[11] = (byte)(this.pidAndIncrement >> 0x18);

			return buffer;
		}

		public override string ToString()
		{
			return string.Format("{0:X8}{1:X8}{2:X8}", (uint)this.timeStamp, (uint)this.machineAndPid, (uint)this.pidAndIncrement);
		}
	}
}